package com.denlaku.longan.service.impl;

import com.denlaku.longan.anno.RequiredTx;
import com.denlaku.longan.qo.DashboardQuery;
import com.denlaku.longan.qo.Query;
import com.denlaku.longan.repository.DashboardRepository;
import com.denlaku.longan.service.ComponentService;
import com.denlaku.longan.service.DashboardHisService;
import com.denlaku.longan.service.DashboardService;
import com.denlaku.longan.util.SnowflakeUtils;
import com.denlaku.longan.vo.Component;
import com.denlaku.longan.vo.Dashboard;
import com.denlaku.longan.vo.DashboardHis;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.collections4.ListUtils;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;

/**
 * @author tianx
 */
@Service
public class DashboardServiceImpl implements DashboardService {
	private static final ConcurrentMap<Long, Cache<@NonNull Long, Dashboard>> CACHE = new ConcurrentHashMap<>();

	private static final Function<Long, Cache<@NonNull Long, Dashboard>> TENANT_CACHE_FN = tenantId -> CACHE.computeIfAbsent(
		tenantId, k -> Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(Duration.ofDays(7)).build());

	@Autowired
	private DashboardRepository dashboardRepository;

	@Autowired
	private ComponentService componentService;

	@Autowired
	private DashboardHisService dashboardHisService;

	@Override
	@RequiredTx
	public void add(Dashboard dashboard) {
		dashboard.setId(SnowflakeUtils.nextId());
		dashboardRepository.add(dashboard);
		TENANT_CACHE_FN.apply(dashboard.getTenantId()).put(dashboard.getId(), dashboard);
	}

	@Override
	@RequiredTx
	public void update(Dashboard dashboard) {
		Dashboard old = this.get(Query.of(dashboard.getTenantId(), dashboard.getId()));
		if (old == null) {
			return;
		}
		DashboardHis dashboardHis = newDashboardHis(old);
		dashboardHisService.add(dashboardHis);
		dashboardRepository.update(dashboard);
		TENANT_CACHE_FN.apply(dashboard.getTenantId()).put(dashboard.getId(), dashboard);
	}

	@Override
	public void config(Dashboard dashboard) {
		Long tenantId = dashboard.getTenantId();
		Dashboard old = this.get(Query.of(tenantId, dashboard.getId()));
		if (old == null) {
			return;
		}
		DashboardHis dashboardHis = newDashboardHis(old);
		dashboardHisService.add(dashboardHis);

		dashboardRepository.update(dashboard);
		Long id = dashboard.getId();
		componentService.batchDelete(tenantId, id);

		List<Component> componentList = collectComponents(dashboard);
		componentService.batchAdd(componentList);
		TENANT_CACHE_FN.apply(dashboard.getTenantId()).put(dashboard.getId(), dashboard);
	}

	private List<Component> collectComponents(Dashboard dashboard) {
		List<Component> componentList = new LinkedList<>();
		Deque<Component> deque = new LinkedList<>(ListUtils.emptyIfNull(dashboard.getComponents()));
		Map<Long, Long> idMap = new LinkedHashMap<>();
		while (!deque.isEmpty()) {
			Component component = deque.removeFirst();
			component.setDashboardId(dashboard.getId());
			component.setParentId(component.getParentId() == null ? 0 : component.getParentId());
			// 对于新增的component, ID是前端生成且小于0，这里替换成后端生成的ID
			component.setId(idMap.computeIfAbsent(component.getId(), key -> key > 0 ? key : SnowflakeUtils.nextId()));
			List<Component> children = ListUtils.emptyIfNull(component.getComponents());
			children.forEach(child -> child.setParentId(component.getId()));
			deque.addAll(children);
			componentList.add(component);
		}
		return componentList;
	}

	private DashboardHis newDashboardHis(Dashboard dashboard) {
		DashboardHis dashboardHis = new DashboardHis();
		dashboardHis.setDashboardId(dashboard.getId());
		dashboardHis.setTenantId(dashboard.getTenantId());
		dashboardHis.setDashboard(dashboard);
		return dashboardHis;
	}

	@Override
	@RequiredTx
	public void delete(Query query) {
		dashboardRepository.delete(query);
		componentService.batchDelete(query.getTenantId(), query.getId());
		TENANT_CACHE_FN.apply(query.getTenantId()).invalidate(query.getId());
	}

	@Override
	public Dashboard get(Query query) {
		return TENANT_CACHE_FN.apply(query.getTenantId()).get(query.getId(), k -> {
			Dashboard dashboard = dashboardRepository.get(query);
			if (dashboard == null) {
				return null;
			}
			dashboard.setComponents(getDashboardComponents(query.getTenantId(), query.getId()));
			return dashboard;
		});
	}

	private List<Component> getDashboardComponents(Long tenantId, Long dashboardId) {
		List<Component> components = componentService.list(tenantId, dashboardId);
		final Map<Long, List<Component>> map = new HashMap<>(components.size());
		for (Component component : components) {
			Long parentId = component.getParentId();
			if (parentId != null && parentId != 0) {
				map.computeIfAbsent(parentId, key -> new ArrayList<>()).add(component);
			}
		}
		List<Component> rootComponents = new ArrayList<>(components.size());
		for (Component component : components) {
			Long parentId = component.getParentId();
			if (parentId == null || parentId == 0) {
				rootComponents.add(component);
			}
			component.setComponents(map.getOrDefault(component.getId(), Collections.emptyList()));
		}
		return rootComponents;
	}

	@Override
	public Dashboard getBase(Query query) {
		return dashboardRepository.get(query);
	}

	@Override
	public List<Dashboard> list(DashboardQuery dashboard) {
		return dashboardRepository.list(dashboard);
	}

	@Override
	public void clearTag(Long tenantId, Long tagId) {
		dashboardRepository.clearTag(tenantId, tagId);
	}

}
